package simplescenegraph;

import java.util.List;

import simplescenegraph.assets.Texture;
import simplescenegraph.math.Vector3f;
import simplescenegraph.math.Vertex3f;
import simplescenegraph.math.VertexBuffer3f;

public abstract class PolygonMesh extends SceneObject {
	
	public enum Mode {
		TRIANGLE_STRIP,
		TRIANGLES,
		POLYGON;
	}
	
	private Mode mode;
	
	private Material material;
	
	private VertexBuffer3f data;
	
	private Object userData;
	
	public PolygonMesh(Mode mode) {
		setMode(mode);
		data = new VertexBuffer3f();
	}
	
	public PolygonMesh() {
		this(Mode.TRIANGLE_STRIP);
	}
	
	@Override
	public void draw(Renderer renderer) {
		if (renderer == null) {
			throw new IllegalArgumentException();
		}
		
		registerTextureIfNotRegistered(renderer);
		
		renderer.draw(this);
	}

	private void registerTextureIfNotRegistered(Renderer renderer) {
		Texture texture = material.getTexture();
		
		if (texture != null && !texture.isRegistered()) {
			renderer.registerTexture(texture);
		}
	}
	
	public VertexBuffer3f getData() {
		return data;
	}
	
	/**
	 * Based on pseudo-codes found in (http://www.opengl.org/wiki/Calculating_a_Surface_Normal):
	 */
	protected void calculateVertexesNormals() {
		switch (mode) {
		case TRIANGLE_STRIP:
			calculateVertexesNormalsInTriangleStripMode();
			break;
		case TRIANGLES:
			calculateVertexesNormalsInTrianglesMode();
			break;
		default:
			calculateVertexesNormalsInPolygonMode();
			break;
		}
	}

	/**
	 * Based on Newell's Method:
	 * 
	 * Begin Function CalculateSurfaceNormal (Input Polygon) Returns Vector
 	 * 
     * 		Set Vertex Normal to (0, 0, 0)
 	 * 
   	 * 		Begin Cycle for Index in [0, Polygon.vertexNumber)
 	 * 
     *   		Set Vertex Current to Polygon.verts[Index]
     *   		Set Vertex Next    to Polygon.verts[(Index plus 1) mod Polygon.vertexNumber]
     * 
     *   		Set Normal.x to Sum of Normal.x and (multiply (Current.y minus Next.y) by (Current.z plus Next.z))
     *   		Set Normal.y to Sum of Normal.y and (multiply (Current.z minus Next.z) by (Current.x plus Next.x))
     *   		Set Normal.z to Sum of Normal.z and (multiply (Current.x minus Next.x) by (Current.y plus Next.y))
 	 * 
     * 		End Cycle
 	 * 
   	 * 		Returning Normalize(Normal)
 	 * 
	 * End Function
	 */
	private void calculateVertexesNormalsInPolygonMode() {
		Vector3f normal = new Vector3f();
		
		for (int i = 0; i < data.size() - 1; i++) {
			Vector3f current = data.get(i).getPosition();
			Vector3f next = data.get(i + 1).getPosition();
			
			normal.x += (current.y - next.y) * (current.z + next.z);
			normal.y += (current.z - next.z) * (current.x + next.x);
			normal.z += (current.x - next.x) * (current.y + next.y);
		}
		
		data.setAllNormalsTo(normal);
	}

	/**
	 * Begin Function CalculateSurfaceNormal (Input Triangle) Returns Vector
 	 * 
	 * 		Set Vector U to (Triangle.p2 minus Triangle.p1)
	 * 		Set Vector V to (Triangle.p3 minus Triangle.p1)
 	 * 
	 * 		Set Normal.x to (multiply U.y by V.z) minus (multiply U.z by V.y)
	 * 		Set Normal.y to (multiply U.z by V.x) minus (multiply U.x by V.z)
	 * 		Set Normal.z to (multiply U.x by V.y) minus (multiply U.y by V.x)
 	 * 
	 * 		Returning Normal
 	 * 
	 * End Function
	 */
	private void calculateVertexesNormalsInTrianglesMode() {
		for (int i = 0; i < data.size(); i += 3) {
			Vertex3f v1 = data.get(i);
			Vertex3f v2 = data.get(i + 1);
			Vertex3f v3 = data.get(i + 2);
			
			Vector3f p1 = v1.getPosition();
			Vector3f p2 = v2.getPosition();
			Vector3f p3 = v3.getPosition();
			
			Vector3f normal = getNormalFromTrianglePoints(p1, p2, p3);
			
			v1.setNormal(normal);
			v2.setNormal(normal);
			v3.setNormal(normal);
		}
	}

	/**
	 * Begin Function CalculateSurfaceNormal (Input Triangle) Returns Vector
 	 * 
	 * 		Set Vector U to (Triangle.p2 minus Triangle.p1)
	 * 		Set Vector V to (Triangle.p3 minus Triangle.p1)
 	 * 
	 * 		Set Normal.x to (multiply U.y by V.z) minus (multiply U.z by V.y)
	 * 		Set Normal.y to (multiply U.z by V.x) minus (multiply U.x by V.z)
	 * 		Set Normal.z to (multiply U.x by V.y) minus (multiply U.y by V.x)
 	 * 
	 * 		Returning Normal
 	 * 
	 * End Function
	 */
	private void calculateVertexesNormalsInTriangleStripMode() {
		Vertex3f v1 = data.get(0);
		Vertex3f v2 = data.get(1);
		for (int i = 2; i < data.size(); i++) {
			Vertex3f v3 = data.get(i);
			
			Vector3f p1 = v1.getPosition();
			Vector3f p2 = v2.getPosition();
			Vector3f p3 = v3.getPosition();
			
			Vector3f normal = getNormalFromTrianglePoints(p1, p2, p3);
			
			v1.setNormal(normal);
			v2.setNormal(normal);
			v3.setNormal(normal);
			
			v1 = v2;
			v2 = v3;
		}
	}

	private Vector3f getNormalFromTrianglePoints(Vector3f p1, Vector3f p2, Vector3f p3) {
		Vector3f u = new Vector3f(p2); u.subtract(p1);
		Vector3f v = new Vector3f(p3); v.subtract(p1);
		
		Vector3f normal = new Vector3f(u.y * v.z - (u.z * v.y),
									   u.z * v.x - (u.x * v.z),
									   u.x * v.y - (u.y * v.x));
		return normal;
	}

	public Mode getMode() {
		return mode;
	}
	
	public void setMode(Mode mode) {
		if (mode == null) {
			throw new IllegalArgumentException();
		}
		
		this.mode = mode;
	}
	
	public Material getMaterial() {
		return material;
	}
	
	public void setMaterial(Material material) {
		this.material = material;
	}
	
	protected Object getUserData() {
		return userData;
	}
	
	protected void setUserData(Object userData) {
		this.userData = userData;
	}
	
	protected void addPolygonVertex(Vertex3f polygonVertex) {
		if (polygonVertex == null) {
			throw new IllegalArgumentException();
		}
		
		data.append(polygonVertex);
	}
	
	protected void calculateBoundingVolume(List<Vector3f> points) {
		if (points == null) {
			throw new IllegalArgumentException();
		}
		
		boundingVolume = new BoundingSphere();
		boundingVolume.centerAtAverage(points);
	}
	
}
